package msc.ai.crypto.jade;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SealedObject;

import jade.core.AID;
import jade.core.Agent;
import jade.core.behaviours.Behaviour;
import jade.core.behaviours.CyclicBehaviour;
import jade.core.behaviours.OneShotBehaviour;
import jade.core.behaviours.TickerBehaviour;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;
import jade.lang.acl.UnreadableException;
import msc.ai.crypto.jade.security.KDC;
import msc.ai.crypto.jade.security.keymanager.AgentPublicKeyDetails;
import msc.ai.crypto.jade.security.keymanager.AgentSharedKeyDetails;

public class OnlineStoreAgent extends Agent
{
	/**
	 * 
	 */
	private static final long serialVersionUID = -4870507068087894974L;
	
	private HashMap<String,SalesItem> salesItemList;
	private ArrayList<AID> pGatewayList = new ArrayList<AID>();
	private ArrayList<AID> kyMgrList = new ArrayList<AID>();
	
	private OnlineStoreGui storeGui;
	private AgentPublicKeyDetails myKeyMgrKeys;
	
	
	
	protected void setup() 
	{

		salesItemList = new HashMap<String, SalesItem>();

		storeGui = new OnlineStoreGui(this);
		storeGui.showGui();

		DFAgentDescription dfd = new DFAgentDescription();
		dfd.setName(getAID());
		ServiceDescription sd = new ServiceDescription();
		sd.setType("online-store");
		sd.setName("online-store-trading");
		dfd.addServices(sd);				 
		
		try 
		{
			DFService.register(this, dfd);
		}
		catch (FIPAException fe) 
		{
			fe.printStackTrace();
		}	
		
		addBehaviour(new ItemRequestBehavior());
		
		addBehaviour(new TickerBehaviour(this, 30000) 
		{
		      protected void onTick() 
		      {
		        
		        DFAgentDescription template = new DFAgentDescription();
		        ServiceDescription sd = new ServiceDescription();
		        sd.setType("payment-gateway");
		        template.addServices(sd);
		        try 
		        {
		          DFAgentDescription[] result = DFService.search(myAgent, template);
		          pGatewayList.clear();
		          for (int i = 0; i < result.length; ++i) 
		          {
		        	  pGatewayList.add(result[i].getName());  
		          }
		        }
		        catch (FIPAException fe) 
		        {
		          fe.printStackTrace();
		        }
		      }
	    } );
		
		
		// Find Key Managers
		addBehaviour(new TickerBehaviour(this, 10000) 
			{
				protected void onTick() 
				{	
						DFAgentDescription template = new DFAgentDescription();
						ServiceDescription sd = new ServiceDescription();
						sd.setType("key-manager");
						template.addServices(sd);
						try 
						{
							DFAgentDescription[] result = DFService.search(myAgent, template);
							boolean isMyKeyMgrAlive = false;
							
							kyMgrList.clear();
							for (int i = 0; i < result.length; ++i) 
							{
								kyMgrList.add(result[i].getName());
								
								if (myKeyMgrKeys != null && (result[i].getName().equals(myKeyMgrKeys.getKeyMgrAID())))
								{
									isMyKeyMgrAlive = true;
								}
								
							}
							
							// need to register ?
							if (myKeyMgrKeys == null)
							{
								// Discard old shared keys with old key manager
								myAgent.clearSharedKeyStore();
								myAgent.addBehaviour(new KeyManagerRegisterBehavior());
							}
							else if (isMyKeyMgrAlive == false)
							{
								// Old Key Manager is dead. Register with a new key manger
								// Discard old shared keys with old key manager
								myAgent.clearSharedKeyStore();							
								myAgent.addBehaviour(new KeyManagerRegisterBehavior());
							}			
							
						}
						catch (FIPAException fe) 
						{
							fe.printStackTrace();
						}
					
				}
			} 
		);
		
		
	}
	
	protected void takeDown() 
	{
		
		try 
		{
			DFService.deregister(this);
		}
		catch (FIPAException fe) 
		{
			fe.printStackTrace();
		}
		
		this.storeGui.dispose();
		
		System.out.println("Online-Store-Agent "+getAID().getName()+" terminating.");
	}
	
	
	public  HashMap<String, SalesItem> getSellItemList()
	{
		return salesItemList;
	}

	public void addItem(final SalesItem item)
	{
		addBehaviour(new OneShotBehaviour() 
		{			
		
			@Override
			public void action()
			{
				System.out.println( getAID().getLocalName() + ": " + item.getItemQty() + " items of Item " + item.getItemName() + " priced at " + item.getItemPrice() + " added" );
				
				if (salesItemList == null ) salesItemList =  new HashMap<String, SalesItem>();
				
				salesItemList.put(item.getItemName(),item);
				
			}
			
		});
		
	}
	
	/**
	 * 
	 * @author Viraj Dayarathne
	 * <p>
	 * Handles requests to buy items and then hands over the process to other behaviours
	 * </p>
	 *
	 */
	private class ItemRequestBehavior extends CyclicBehaviour 
	{
		public void action() 
		{
			MessageTemplate mt = MessageTemplate.MatchPerformative(ACLMessage.CFP);
			ACLMessage msg = myAgent.receive(mt, KDC.getKey()); //TODO: this is unsecure message
			if (msg != null) 
			{

				String itemName = msg.getContent();
				ACLMessage reply = msg.createReply();

				SalesItem item = (SalesItem) salesItemList.get(itemName);
				if (item != null) 
				{
					if (item.getItemQty() <= 0)
					{
						reply.setPerformative(ACLMessage.REFUSE);
						reply.setContent("stock-over");
					}
					else
					{
						// EDIT : Preparing to offer. Need to get a session key from the key manager first
						// Then should use that key to encode the messation.
						
						reply.setPerformative(ACLMessage.PROPOSE);
						try
						{
							reply.setContentObject(item);
							myAgent.addBehaviour(new ItemSalesBehavior(item));
							
						} catch (IOException e)
						{
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
					
				}
				else 
				{	
					reply.setPerformative(ACLMessage.REFUSE);
					reply.setContent("not-available");
				}
				// TODO : Remove unnecessary codes from this class.
				
				// Get the shared key first!!!
//				Key sharedKey = null;
//				if (myAgent.isKeyExists(msg.getSender().getLocalName()))
//				{
//					sharedKey = myAgent.getKey(msg.getSender().getLocalName(), msg.getSender().getLocalName());
//				}
//				else
//				{	
//					// Shared key not found, get a key from Key Manager
//				}
				
				// This should call another behavior to send the reply to the sender.
				// This way we can first get the shared key and then send the offer.
				
				
				reply.setSecureMsg(true);				
				//myAgent.send(reply, sharedKey); // This is secure if it's offering item
				
				//NOTE: Sending the reply is handed over to ItemOfferBehavior
				myAgent.addBehaviour(new ItemOfferBehavior(msg, reply));
				
				
			}
			else 
			{
				block();
			}
		}
	} // End of ItemRequestBehavior
	
	
	private class ItemOfferBehavior extends Behaviour
	{
		private ACLMessage requestMsg;
		private ACLMessage offerMsg;
		private Key sharedKey;
		private int offerStep = 0;
		
		public ItemOfferBehavior(ACLMessage requetMsg,ACLMessage offerReplyMsg)
		{
			this.requestMsg = requetMsg;
			this.offerMsg = offerReplyMsg;
			
			
			
		}
		
		@Override
		public void action()
		{
			// Check for the key and then get the key to send the message
			
			if (myAgent.isKeyExists(this.requestMsg.getSender().getLocalName()))
			{
				this.sharedKey = myAgent.getKey(this.requestMsg.getSender().getLocalName(), this.requestMsg.getSender().getLocalName());
			}
			else
			{	
				// Shared key not found, get a key from Key Manager
				// first block this behavior
				this.block();
				
				myAgent.addBehaviour(new GetSharedKeyBehavior(this.requestMsg.getSender(), this));
			}
			
			if (offerMsg != null && sharedKey != null)
			{
				myAgent.send(this.offerMsg, this.sharedKey);
				this.offerStep = 1;
			}
		}

		@Override
		public boolean done()
		{
			// TODO Auto-generated method stub
			if (this.offerStep == 1 ) return true;
			return false;
		}
		
	}
	
	private class ItemSalesBehavior extends Behaviour //CyclicBehaviour
	{private int salesStep = 0;
		private SalesItem sItem;
		private BuyingItem bItem;
		public ItemSalesBehavior(SalesItem sItem)
		{
			this.sItem = sItem;
		} 
		
		@Override
		public void action()
		{
			// validate the the buying details and send purchase response.
			MessageTemplate mt = MessageTemplate.MatchPerformative(ACLMessage.ACCEPT_PROPOSAL);
			ACLMessage msg = myAgent.receive(mt, KDC.getKey());
			if (msg != null) 
			{
				try
				{
					System.out.println(getAID().getLocalName() + ": Proposal Accepted. Initiating sales process.");
					bItem = (BuyingItem )msg.getContentObject();
					
					myAgent.addBehaviour(new SalesProcessBehavior(msg, bItem,sItem));
					
				} catch (UnreadableException e)
				{
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				
			}
			else 
			{
				block();
			}
			salesStep=1;
			
		}

		@Override
		public boolean done() {
			// TODO Auto-generated method stub
			if(salesStep==1){
				return true;
			}
			return false;
		}
		
	}
	
	private class SalesProcessBehavior extends Behaviour
	{
		private SalesItem sItem;
		private BuyingItem bItem;
		private int salesQty;
		private double totalPrice;
		private int step = 0;
		private ItemSalesBehavior itmBehavior;
		
		private ACLMessage shopperMsg;
		private MessageTemplate mt;
		
		public SalesProcessBehavior(ACLMessage apMsg, BuyingItem bItem,SalesItem sItem)
		{
			this.shopperMsg = apMsg;
			this.bItem = bItem;
			this.sItem = sItem;
			System.out.println(getAID().getLocalName() + ": Sales process initiated.");
		}

		@Override
		public void action()
		{
			// TODO Auto-generated method stub
			switch (step)
			{
				case 0:					
					// send payment gateway request
					// calc sales price and qty
					if (bItem.getItemQtyRequired() > sItem.getItemQty()) salesQty =sItem.getItemQty();
					else salesQty = bItem.getItemQtyRequired();
					
					totalPrice = salesQty * sItem.getItemPrice();
					
					System.out.println(getAID().getLocalName() + ": Sending Payment Gateway Authorization. Tot Price : " + totalPrice 
									+ ", Qty : " + salesQty + ", ItemPrice : " +  sItem.getItemPrice());
					
					BuyingItem finalBItem = new BuyingItem();
					
					finalBItem.setCreditCardNo(bItem.getCreditCardNo());
					finalBItem.setCreditCardType(bItem.getCreditCardType());
					finalBItem.setCreditCVVNo(bItem.getCreditCVVNo());
					finalBItem.setItemQtyRequired(salesQty);
					finalBItem.setItemSearching(bItem.getItemSearching());
					finalBItem.setMaxItemPrice(sItem.getItemPrice());
					
					
					try
					{
						ACLMessage req = new ACLMessage(ACLMessage.REQUEST);
						for (int i = 0; i < pGatewayList.size(); ++i) 
						{
							//TODO: Select only one???
							req.addReceiver((AID)pGatewayList.get(i));
						}
						req.setContentObject(finalBItem);
						req.setConversationId("cc-authorization");
						req.setReplyWith("request"+System.currentTimeMillis()); // Unique value
						// Get Payment Gateway Shared Key
						myAgent.send(req, KDC.getKey()); // this is secure
				          
						mt = MessageTemplate.and(
								MessageTemplate.MatchConversationId("cc-authorization"),
								MessageTemplate.MatchInReplyTo(req.getReplyWith())
							);
						step = 1;
						
					}
					catch (Exception e) 
					{
						// TODO: handle exception
						e.printStackTrace();
						step = 2;
					}
					
					
					break;
					
				case 1:
					// wait for payment gateway reply
					// if replied ok, then update the inventory
					// Need the Shared Key of Payment Gateway
					ACLMessage reply = myAgent.receive(mt, KDC.getKey()); // this is secure
					if (reply != null) 
					{
						System.out.println(getAID().getLocalName() + ": CC Auth Reply Recieved"); 
						
						// send the reply
						ACLMessage shpReply = shopperMsg.createReply();
						if (reply.getPerformative() == ACLMessage.INFORM)
						{
							salesItemList.get(sItem.getItemName()).sellItem(salesQty);
							shpReply.setPerformative(ACLMessage.INFORM);
							System.out.println(getAID().getLocalName() + ": " + bItem.getItemSearching() +" sold to agent "+ shopperMsg.getSender().getName());
						}
						else 
						{					
							System.out.println(getAID().getLocalName() + ": " + " Credit Card Authorization Failed.");
							shpReply.setPerformative(ACLMessage.FAILURE);
							shpReply.setContent(reply.getContent());
						}
						// Need the shared key of Shopper
						myAgent.send(shpReply, KDC.getKey()); // this is secure
						
					}
					
										
					step = 2;
					break;
					
				case 2:
					break;
				default:
					break;
			}
		}

		@Override
		public boolean done()
		{
			// TODO Auto-generated method stub
			if (step == 2) return true;
			return false;
		}
		
	}
	
	
	private class KeyManagerRegisterBehavior extends Behaviour
	{

		/**
		 * 
		 */
		private static final long serialVersionUID = -768575700555407668L;
				
		private int registerStep = 0;		
		private MessageTemplate mt;
		
		@Override
		public void action()
		{
			switch (registerStep)
			{
				case 0:	
					// Register with Key Manager
					System.out.println(getAID().getLocalName() + ": Key Manager Registration started");
					ACLMessage req = new ACLMessage(ACLMessage.REQUEST);
					for (int i = 0; i < kyMgrList.size(); ++i) 
					{
						//TODO: Select only one???
						req.addReceiver((AID)kyMgrList.get(i));
					}
					
					req.setConversationId("keymgr_register");
					req.setReplyWith("register_"+System.currentTimeMillis()); // Unique value
//					req.setSecureMsg(false);
					myAgent.send(req); // send unsecured message
			          
					mt = MessageTemplate.and(
							MessageTemplate.MatchPerformative(ACLMessage.INFORM),
							MessageTemplate.and(
									MessageTemplate.MatchConversationId("keymgr_register_done"),
									MessageTemplate.MatchInReplyTo(req.getReplyWith())
							)
						);
					
					registerStep = 1;
					break;
					
				case 1:
					// Receive Key Details
					ACLMessage keyMgrReply = myAgent.receive(mt); // this is unsecure
					if (keyMgrReply != null) 
					{
						System.out.println(getAID().getLocalName() + ": Key Manager Regisration Done"); 
						try
						{
							AgentPublicKeyDetails pkDetails = (AgentPublicKeyDetails) keyMgrReply.getContentObject();							
							myKeyMgrKeys = pkDetails;
							
							System.out.println(getAID().getLocalName() + ": Key Manager " + myKeyMgrKeys.getKeyMgrAID().getLocalName());
							registerStep = 2;
							
						} catch (UnreadableException e)
						{
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
						
						
					}
					else
					{
						//TODO block(); ?
					}
					break;
					
				default:
					break;
			}
			
		}

		@Override
		public boolean done()
		{
			 
			if (registerStep == 2) 
			{
				return true;
			}
			return false;
		}
		
		
	} // End of KeyManagerRegisterBehavior
	
	
	private class GetSharedKeyBehavior extends Behaviour
	{
		private AID otherParty;
		private Behaviour callingBehaviour;
		
		private int sharedKeyStep = 0;
		private MessageTemplate mt;
		private AgentSharedKeyDetails sharedKeyDet;
		private SealedObject sharedKeySealed;
		
		
		
		public GetSharedKeyBehavior(AID otherParty, Behaviour callingBehavior )
		{			
			this.otherParty = otherParty;
			this.callingBehaviour = callingBehavior;
			
			
					
			
		}

		@Override
		public void action()
		{
			
			// Create the shared key details and encrypt it;
			this.sharedKeyDet = new AgentSharedKeyDetails(this.myAgent.getAID(),this.otherParty);
			
			// Encrypting			
			try
			{
//				Cipher cpE = Cipher.getInstance("RSA/ECB/PKCS1Padding");
				Cipher cpE = Cipher.getInstance("RSA");
				cpE.init(Cipher.ENCRYPT_MODE, myKeyMgrKeys.getKeyMgrPublicKey());

				this.sharedKeySealed = new SealedObject(sharedKeyDet,cpE);
				
			} catch (InvalidKeyException e1)
			{
				// TODO Auto-generated catch block
				
				e1.printStackTrace();
			} catch (NoSuchAlgorithmException e)
			{
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (NoSuchPaddingException e)
			{
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IllegalBlockSizeException e)
			{
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IOException e)
			{
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			finally
			{
				sharedKeyStep = 3;
			}
				
			
			
			switch (sharedKeyStep)
			{
				case 0:
					// Request a shared key from KeyManager
					System.out.println(getAID().getLocalName() + ": Shared Key Request for " + this.otherParty.getLocalName());
					ACLMessage req = new ACLMessage(ACLMessage.REQUEST);
					
					req.setConversationId("shared_key_request");
					req.setReplyWith("shared_key_"+this.otherParty.getLocalName()+ "_" +System.currentTimeMillis()); // Unique value
//					req.setSecureMsg(false);
					
					// Add the sealed object
					
					try
					{
						req.setContentObject(this.sharedKeySealed);
						// send unsecured message
						// The object is sealed, hence no need to set as secure message
						myAgent.send(req); 
				          
						// set the reply message to capture
						mt = MessageTemplate.and(
								MessageTemplate.MatchPerformative(ACLMessage.INFORM),
								MessageTemplate.and(
										MessageTemplate.MatchConversationId("shared_key_response"),
										MessageTemplate.MatchInReplyTo(req.getReplyWith())
								)
							);
						
						sharedKeyStep = 1;
						
					} catch (IOException e)
					{
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
					
				
					break;
					
				case 1:
					// Receive a shared key from KeyManager
					
					ACLMessage keyMgrReply = myAgent.receive(mt); // this is unsecure
					if (keyMgrReply != null) 
					{
						System.out.println(getAID().getLocalName() + ": Shared Key Recieved from KeyManager " + keyMgrReply.getSender().getLocalName() ); 
						try
						{
						
							// Get the sealed object from KDC Reply
							SealedObject seldObj = (SealedObject) (keyMgrReply.getContentObject()); 
														
							// Unpack the sealed object from my private key
//							Cipher cpD = Cipher.getInstance("RSA/ECB/PKCS1Padding");
							Cipher cpD = Cipher.getInstance("RSA");
							cpD.init(Cipher.DECRYPT_MODE, myKeyMgrKeys.getMyKeyPair().getPrivate() );
							AgentSharedKeyDetails tmpShrdKey = (AgentSharedKeyDetails) seldObj.getObject(cpD);
							
							System.out.println(getAID().getLocalName() + ": got shared key " +  tmpShrdKey.getSharedKey());
							
							
							// Send the shared key to other party							
							ACLMessage sharedKeyMsg = new ACLMessage(ACLMessage.INFORM);
							sharedKeyMsg.setConversationId("shared_key_share");
							sharedKeyMsg.setReplyWith("shared_key_share"+this.otherParty.getLocalName()+ "_" +System.currentTimeMillis()); // Unique value//							
													
							
							sharedKeyStep = 2;
							
						} catch (UnreadableException e)
						{
							// TODO Auto-generated catch block
							e.printStackTrace();
						} catch (NoSuchAlgorithmException e)
						{
							// TODO Auto-generated catch block
							e.printStackTrace();
						} catch (NoSuchPaddingException e)
						{
							// TODO Auto-generated catch block
							e.printStackTrace();
						} catch (InvalidKeyException e)
						{
							// TODO Auto-generated catch block
							e.printStackTrace();
						} catch (IllegalBlockSizeException e)
						{
							// TODO Auto-generated catch block
							e.printStackTrace();
						} catch (BadPaddingException e)
						{
							// TODO Auto-generated catch block
							e.printStackTrace();
						} catch (IOException e)
						{
							// TODO Auto-generated catch block
							e.printStackTrace();
						} catch (ClassNotFoundException e)
						{
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
						
						
					}
					else
					{
						//TODO block(); ?
					}
					
					
					break;
					
					
				case 2:
					// Receive acknowledgment from other party
					
					// Store the shared key in the Store
					
					// stop this behavior and pass over to calling behavior
					sharedKeyStep = 3;
					if (this.callingBehaviour != null)
					{
						this.callingBehaviour.restart();
					}
					break;

				default:
					break;
			}
			
		}

		@Override
		public boolean done()
		{
			
			if (sharedKeyStep == 3) 
			{
				return true;
			}
			return false;
		}
		
		
	} // End of
	
}
